"""
media (:mod:`skrf.media.media`)
========================================

Media class.

.. autosummary::
    :toctree: generated/

    Media
    DefinedGammaZ0

"""
from __future__ import annotations

import re
import warnings
from abc import ABC, abstractmethod
from copy import deepcopy as copy
from numbers import Number
from pathlib import Path

import numpy as np
from numpy import any, array, gradient, imag, ones, real
from scipy import stats
from scipy.constants import c

from .. import mathFunctions as mf
from ..constants import INF, S_DEF_DEFAULT, ZERO, NumberLike, to_meters
from ..frequency import Frequency
from ..network import Network, connect, impedance_mismatch, innerconnect


class Media(ABC):
    """
    Abstract Base Class for a single mode on a transmission line media.


    This class init's with `frequency` and `z0_port` (the port impedance);
    attributes shared by all media. Methods defined here make use of the
    properties :

    * `gamma` - (complex) media propagation constant
    * `z0` - (complex) media characteristic impedance

    Which define the properties of a specific media. Any sub-class of Media
    must implement these properties. `gamma` and `z0` should return
    complex arrays of the same length as `frequency`. `gamma` must
    follow the convention:

    * positive real(gamma) = attenuation
    * positive imag(gamma) = forward propagation

    Parameters
    ----------
    frequency : :class:`~skrf.frequency.Frequency` object or None
        frequency band of this transmission line medium.
        Defaults to None, which produces a 1-10ghz band with 101 points.
    z0_port : number, array-like, or None
        `z0_port` is the port impedance for networks generated by the media.
        If `z0_port` is not None, the networks generated by the media are
        renormalized (or in other words embedded) from the characteristic
        impedance z0 of the media to `z0_port`.
        Else if `z0_port` is None, the networks port impedances will be the raw
        characteristic impedance z0 of the media.
        (Default is None)
    z0_override : number, array-like, or None
        `z0_override` override the characteristic impedance for the media.
        If `z0_override` is not None, the networks generated by the media have
        their characteristic impedance `z0` overridden by `z0_override`.
        (Default is None)
    z0 : number, array-like, or None
        deprecated parameter, alias to `z0_override` if `z0_override` is None.
        Emit a deprecation warning.


    Notes
    -----
    Full-wave simulations results can be networks with complex and
    frequency-dependant `z0` port impedances.
    However, the regular touchstone (s-parameters) files generated by most VNA
    measurements are normalized to a single real value, e. g. 1. or 50 ohm.
    Manually setting the `z0_port` to a single real value enforce the second
    behaviour. The default (`z0_port = None`) is the first behaviour.
    """

    def __init__(self, frequency: Frequency | None = None,
                 z0_port: NumberLike | None = None,
                 z0_override: NumberLike | None = None,
                 z0: NumberLike | None = None):
        if frequency is None:
            frequency = Frequency(1,10,101,'GHz')

        if z0 is not None:
            # warns of deprecation
            warnings.warn(
                'Use of `z0` in Media initialization is deprecated.\n'
                'If `z0_override` is None, '
                'then `z0_override` will take z0 value.\n'
                '`z0` will be removed of Media initialization  in version 1.0',
              DeprecationWarning, stacklevel = 2)
            if z0_override is None:
                z0_override = z0

        self.frequency = frequency.copy()
        self.z0_port = z0_port
        self.z0_override = z0_override

    def mode(self,  **kw) -> Media:
        r"""
        Create another mode in this medium.

        Convenient way to return a copy this Media object
        with eventually different properties.

        Parameters
        ----------
        \*\*kwargs : keyword arguments passed to the copy

        Returns
        -------
        copy : :class:`Media`
            A copy of this Media object with \*\*kwargs attribute
        """
        out = copy(self)
        for k in kw:
            setattr(out, k, kw[k])
        return out

    def copy(self) -> Media:
        """
        Copy of this Media object.

        Returns
        -------
        copy : :class:`Media`
            A copy of this Media object
        """
        return copy(self)

    def __eq__(self,other):
        """
        Test for numerical equality (up to :data:`~skrf.constants.ZERO`).
        """

        if self.frequency != other.frequency:
            return False

        if max(abs(self.z0 - other.z0)) > ZERO:
            return False

        if max(abs(self.gamma - other.gamma)) > ZERO:
            return False

        if self.z0_port is None and other.z0_port is not None:
            if max(abs(self.z0 - other.z0_port)) > ZERO:
                return False

        return True

    def __len__(self) -> int:
        """
        Length of frequency axis.
        """
        return len(self.frequency)

    @property
    def npoints(self) -> int:
        """
        Number of points of the frequency axis.

        Returns
        -------
        npoints : int
            Number of points of the frequency axis.
        """
        return self.frequency.npoints

    @npoints.setter
    def npoints(self, val):
        self.frequency.npoints = val

    @property
    def z0_port(self) -> np.ndarray:
        """
        Port Impedance.

        Returns
        -------
        z0_port : :class:`numpy.ndarray`
        """
        if self._z0_port is None:
            return self._z0_port
        return self._z0_port*ones(len(self))

    @z0_port.setter
    def z0_port(self, val):
        self._z0_port = val

    @property
    def z0_override(self) -> np.ndarray:
        """
        Port Impedance.

        Returns
        -------
        z0_port : :class:`numpy.ndarray`
        """
        if self._z0_override is None:
            return self._z0_override
        return self._z0_override*ones(len(self))

    @z0_override.setter
    def z0_override(self, val):
        self._z0_override = val

    @property
    @abstractmethod
    def gamma(self):
        r"""
        Propagation constant.

        In skrf, defined as :math:`\gamma = \alpha + j \beta`.

        Returns
        -------
        gamma : :class:`numpy.ndarray`
            complex propagation constant for this media

        Notes
        -----
        `gamma` must adhere to the following convention:

         * positive real(gamma) = attenuation
         * positive imag(gamma) = forward propagation
        """
        return None


    @property
    def alpha(self) -> np.ndarray:
        """
        Real (attenuation) component of gamma.

        Returns
        -------
        alpha : :class:`numpy.ndarray`
        """
        return real(self.gamma)

    @property
    def beta(self) -> np.ndarray:
        """
        Imaginary (propagating) component of gamma.

        Returns
        -------
        beta : :class:`numpy.ndarray`
        """
        return imag(self.gamma)

    @property
    @abstractmethod
    def z0_characteristic(self):
        """
        Characteristic Impedance, :math:`z_0`.
        This abstract method has to be defined in the Media Class.

        Returns
        -------
        z0_characteristic : np.ndarray
            Characteristic Impedance in units of ohms
        """
        return None

    @property
    def z0(self):
        """
        Return Characteristic Impedance `z0_characteristic`.
        If `z0_override` is not None, it is returned instead.

        Returns
        -------
        z0 : np.ndarray
            z0_characteristic or z0_override in units of ohms
        """
        if self.z0_override is None:
            return self.z0_characteristic
        else:
            return self.z0_override

    # left for backward compatibility
    @property
    def Z0(self):
        """
        Characteristic Impedance

        Returns
        -------
        z0 : np.ndarray
            Characteristic Impedance in units of ohms
        """
        # warns of deprecation
        warnings.warn(
            'Use of `Z0` (uppercase) is deprecated and return `z0`.\n'
            'Use characteristic impedance `z0` (lowercase) instead.\n'
            '`Z0` will be removed in version 1.0.',
          DeprecationWarning, stacklevel = 2)
        return self.z0


    @property
    def v_p(self) -> np.ndarray:
        r"""
        Complex phase velocity (in m/s).

        .. math::
            j \cdot \omega / \gamma

        Notes
        -----
        The `j` is used so that real phase velocity corresponds to
        propagation

        where:

        * :math:`\omega` is angular frequency (rad/s),
        * :math:`\gamma` is complex propagation constant (rad/m)

        Returns
        -------
        v_p : :class:`numpy.ndarray`

        See Also
        --------
        propagation_constant
        gamma

        """
        return 1j*(self.frequency.w/self.gamma)


    @property
    def v_g(self):
        r"""
        Complex group velocity (in m/s).

        .. math::
            j \cdot d \omega / d \gamma


        where:

        * :math:`\omega` is angular frequency (rad/s),
        * :math:`\gamma` is complex propagation constant (rad/m)

        Notes
        -----
        the `j` is used to make propagation real, this is needed because
        skrf defined the gamma as :math:`\gamma= \alpha +j\beta`.

        Returns
        -------
        v_g : :class:`numpy.ndarray`

        References
        ----------
        https://en.wikipedia.org/wiki/Group_velocity

        See Also
        --------
        propagation_constant
        v_p
        gamma
        """
        dw = self.frequency.dw
        dk = gradient(self.gamma)

        return dw/dk


    def get_array_of(self, x):
        try:
            if len(x)!= len(self):
                # we have to make a decision
                pass
        except(TypeError):
            y = x* ones(len(self))

        return y

    ## Other Functions
    def theta_2_d(self, theta: NumberLike, deg:bool = True, bc: bool = True) -> NumberLike:
        r"""
        Convert electrical length to physical distance.

        The electrical length is given by :math:`d=\theta/\beta`.

        The given electrical length can be given either at the center frequency
        or on the entire band depending of the parameter `bc`.

        Parameters
        ----------
        theta : number
            electrical length, at band center (see deg for unit)
        deg : Boolean, optional
            is theta in degrees?
            Default is True (theta is assumed in degrees)
        bc : bool, optional.
            evaluate only at band center, or across the entire band?
            Default is True (evaluation assumed at band center)

        Returns
        --------
        d : number, array-like
            physical distance in meters

        """
        if deg:
            theta = mf.degree_2_radian(theta)

        gamma = self.gamma
        if bc:
                return 1.0*theta/np.imag(gamma[int(gamma.size/2)])
        else:
                return 1.0*theta/np.imag(gamma)

    def electrical_length(self, d: NumberLike, deg: bool = False) -> NumberLike:
        r"""
        Calculate the complex electrical length for a given distance.

        Electrical length is given by :math:`\theta=\gamma d`.

        Parameters
        ----------
        d: number or array-like
            delay distance, in meters
        deg: Boolean, optional
            return electrical length in deg?
            Default is False (returns electrical length in radians)

        Returns
        -------
        theta: number or array-like
            complex electrical length in radians or degrees, depending on
            value of deg.
        """
        gamma = self.gamma

        if not deg:
            return gamma*d
        else:
            return  mf.radian_2_degree(gamma*d)

    ## Network creation

    # lumped elements
    def match(self, nports: int = 1, z0: NumberLike | None = None,
              z0_norm: bool = False, **kwargs) -> Network:
        r"""
        Perfect matched load (:math:`\Gamma_0 = 0`).

        Parameters
        ----------
        nports : int
            number of ports
        z0 : number, or array-like or None
            port impedance. This sets the resultant
            :class:`~skrf.network.Network`’s
            :attr:`~skrf.network.Network.z0`.
            Default is None, the Media’s :attr:`~skrf.media.Media.z0_port`
            value is used. If :attr:`~skrf.media.Media.z0_port` is
            also None (by default), in which case the Media's native
            :attr:`~skrf.media.Media.z0` is used.
            A Media's :attr:`~skrf.media.Media.z0_port` is an optional
            attribute for renormalizing resultant
            :class:`~skrf.network.Network` to a new
            reference impedance that differs from a Media's native
            :attr:`~skrf.media.Media.z0`.
        z0_norm : bool
            is z0 normalized to this media's `z0`?
        \*\*kwargs : key word arguments
            passed to :class:`~skrf.network.Network` initializer

        Returns
        -------
        match : :class:`~skrf.network.Network` object
            a n-port match

        Examples
        --------
        >>> my_match = my_media.match(2,z0 = 50, name='Super Awesome Match')

        """
        result = Network(**kwargs)
        result.frequency = self.frequency
        result.s = np.zeros((self.frequency.npoints, nports, nports), dtype=complex)
        result.port_modes = np.array(["S"] * result.nports)
        if z0 is None:
            if self.z0_port is None:
                z0 = self.z0
            else:
                z0 = self.z0_port
        else:
            if isinstance(z0, str):
                z0 = parse_z0(z0)*self.z0
            if z0_norm:
                z0 = z0*self.z0

        result.z0 = z0
        return result

    def load(self, Gamma0: NumberLike, nports: int = 1,
             z0: NumberLike | None = None, **kwargs) -> Network:
        r"""
        Load of given reflection coefficient.

        Parameters
        ----------
        Gamma0 : number, array-like
            Reflection coefficient of load (linear, not in db). If its
            an array it must be of shape: `kxnxn`, where k is number of frequency
            points in media, and n is `nports`
        nports : int
            number of ports
        z0 : number, or array-like or None
            port impedance. This sets the resultant
            :class:`~skrf.network.Network`’s
            :attr:`~skrf.network.Network.z0`.
            Default is None, the Media’s :attr:`~skrf.media.Media.z0_port`
            value is used. If :attr:`~skrf.media.Media.z0_port` is
            also None (by default), in which case the Media's native
            :attr:`~skrf.media.Media.z0` is used.
            A Media's :attr:`~skrf.media.Media.z0_port` is an optional
            attribute for renormalizing resultant
            :class:`~skrf.network.Network` to a new
            reference impedance that differs from a Media's native
            :attr:`~skrf.media.Media.z0`.
        \*\*kwargs : key word arguments
            passed to :func:`match`, which is called initially to create a
            'blank' network.

        Returns
        -------
        load : :class:`~skrf.network.Network` object
            n-port load, where  S = Gamma0*eye(...)

        See Also
        --------
        match
        open
        short
        """
        result = self.match(nports, z0 = z0, **kwargs)
        result.s = np.array(Gamma0).reshape(-1, 1, 1) * \
            np.eye(nports, dtype=complex).reshape((-1, nports, nports)).\
            repeat(self.frequency.npoints, 0)
        result.port_modes = np.array(["S"] * result.nports)
        return result

    def short(self, nports: int = 1,
              z0: NumberLike | None = None, **kwargs) -> Network:
        r"""
        Short (:math:`\Gamma_0 = -1`)

        For s_def = 'power' (:math:`-Z_{ref}^*/Z_{ref}`)

        Parameters
        ----------
        nports : int
            number of ports
        \*\*kwargs : key word arguments passed to :func:`load`.
        z0 : number, or array-like or None
            port impedance. This sets the resultant
            :class:`~skrf.network.Network`’s
            :attr:`~skrf.network.Network.z0`.
            Default is None, the Media’s :attr:`~skrf.media.Media.z0_port`
            value is used. If :attr:`~skrf.media.Media.z0_port` is
            also None (by default), in which case the Media's native
            :attr:`~skrf.media.Media.z0` is used.
            A Media's :attr:`~skrf.media.Media.z0_port` is an optional
            attribute for renormalizing resultant
            :class:`~skrf.network.Network` to a new
            reference impedance that differs from a Media's native
            :attr:`~skrf.media.Media.z0`.
        Returns
        -------
        match : :class:`~skrf.network.Network` object
            a n-port short circuit

        Notes
        -----
        This calls ::

            load(-1.0, nports, **kwargs)

        See Also
        --------
        match
        open
        load
        """
        if z0 is None:
            if self.z0_port is None:
                z0 = self.z0
            else:
                z0 = self.z0_port
        s_short = -1
        # Powerwave short is not necessarily -1
        if kwargs.get('s_def', S_DEF_DEFAULT) == 'power':
            s_short = -np.conjugate(z0) / z0
        return self.load(s_short, nports, z0 = z0, **kwargs)

    def open(self, nports: int = 1, **kwargs) -> Network:
        r"""
        Open (:math:`\Gamma_0 = 1`).

        Parameters
        ----------
        nports : int
            number of ports
        \*\*kwargs : key word arguments passed to :func:`load`

        Returns
        -------
        match : :class:`~skrf.network.Network` object
            a n-port open circuit

        Notes
        -----
        This calls ::

            load(1.0, nports, **kwargs)

        See Also
        --------
        match
        load
        short
        """

        return self.load(1.0, nports, **kwargs)

    def resistor(self, R: NumberLike, *args, **kwargs) -> Network:
        r"""
        Resistor.

        Parameters
        ----------
        R : number, array
            Resistance , in Ohms. If this is an array, must be of
            same length as frequency vector.
        \*args, \*\*kwargs : arguments, key word arguments
            passed to :func:`match`, which is called initially to create a
            'blank' network.

        Returns
        -------
        resistor : a 2-port :class:`~skrf.network.Network`

        See Also
        --------
        match
        short
        open
        load
        capacitor
        inductor
        """
        s_def = kwargs.pop('s_def', S_DEF_DEFAULT)
        result = self.match(nports=2, s_def='power', **kwargs)
        s = np.zeros(shape=result.s.shape, dtype=complex)
        R = np.array(R)
        # Convert Y-parameters resistor to S-parameters in power wave to accommodate any R value.
        # y[:, 0, 0] = 1.0 / R
        # y[:, 1, 1] = 1.0 / R
        # y[:, 0, 1] = -1.0 / R
        # y[:, 1, 0] = -1.0 / R
        z0_0, z0_1 = result.z0[:, 0], result.z0[:, 1]
        denom = R + (z0_0 + z0_1)
        s[:, 0, 0] = (R - z0_0.conj() + z0_1) / denom
        s[:, 1, 1] = (R + z0_0 - z0_1.conj()) / denom
        s[:, 0, 1] = 2 * (z0_0.real * z0_1.real)**0.5 / denom
        s[:, 1, 0] = 2 * (z0_0.real * z0_1.real)**0.5 / denom
        result.s = s

        # Renormalize into s_def if required
        if s_def != 'power':
            result.renormalize(z_new=result.z0, s_def=s_def)

        return result

    def capacitor(self, C: NumberLike, **kwargs) -> Network:
        r"""
        Capacitor.

        Parameters
        ----------
        C : number, array
            Capacitance, in Farads. If this is an array, must be of
            same length as frequency vector.
        \*\*kwargs : key word arguments
            passed to :func:`match`, which is called initially to create a
            'blank' network.

        Returns
        -------
        capacitor : a 2-port :class:`~skrf.network.Network`

        See Also
        --------
        match
        short
        open
        load
        resistor
        inductor
        """
        s_def = kwargs.pop('s_def', S_DEF_DEFAULT)
        result = self.match(nports=2, s_def='power', **kwargs)
        w = self.frequency.w
        s = np.zeros(shape=result.s.shape, dtype=complex)
        C = np.array(C)
        # Convert Y-parameters capacitor to S-parameters in power wave to accommodate any C value.
        # y[:, 0, 0] = 1j * w * C
        # y[:, 1, 1] = 1j * w * C
        # y[:, 0, 1] = -1j * w * C
        # y[:, 1, 0] = -1j * w * C
        z0_0, z0_1 = result.z0[:, 0], result.z0[:, 1]
        denom = 1.0 + 1j * w * C * (z0_0 + z0_1)
        s[:, 0, 0] = (1.0 - 1j * w * C * (z0_0.conj() - z0_1) ) / denom
        s[:, 1, 1] = (1.0 - 1j * w * C * (z0_1.conj() - z0_0) ) / denom
        s[:, 0, 1] = (2j * w * C * (z0_0.real * z0_1.real)**0.5) / denom
        s[:, 1, 0] = (2j * w * C * (z0_0.real * z0_1.real)**0.5) / denom
        result.s = s

        # Renormalize into s_def if required
        if s_def != 'power':
            result.renormalize(z_new=result.z0, s_def=s_def)

        return result

    def inductor(self, L: NumberLike, **kwargs) -> Network:
        r"""
        Inductor.

        Parameters
        ----------
        L : number, array
            Inductance, in Henries. If this is an array, must be of
            same length as frequency vector.
        \*\*kwargs : key word arguments
            passed to :func:`match`, which is called initially to create a
            'blank' network.

        Returns
        -------
        inductor : a 2-port :class:`~skrf.network.Network`

        See Also
        --------
        match
        short
        open
        load
        capacitor
        resistor
        """
        s_def = kwargs.pop('s_def', S_DEF_DEFAULT)
        result = self.match(nports=2, s_def='power', **kwargs)
        w = self.frequency.w
        s = np.zeros(shape=result.s.shape, dtype=complex)
        L = np.array(L)
        # Convert Y-parameters inductor to S-parameters in power wave to accommodate any L value.
        # y[:, 0, 0] = 1.0 / (1j * w * L)
        # y[:, 1, 1] = 1.0 / (1j * w * L)
        # y[:, 0, 1] = -1.0 / (1j * w * L)
        # y[:, 1, 0] = -1.0 / (1j * w * L)
        z0_0, z0_1 = result.z0[:, 0], result.z0[:, 1]
        denom = (1j * w * L) + (z0_0 + z0_1)
        s[:, 0, 0] = (1j * w * L - z0_0.conj() + z0_1) / denom
        s[:, 1, 1] = (1j * w * L + z0_0 - z0_1.conj()) / denom
        s[:, 0, 1] = 2 * (z0_0.real * z0_1.real)**0.5 / denom
        s[:, 1, 0] = 2 * (z0_0.real * z0_1.real)**0.5 / denom
        result.s = s

        # Renormalize into s_def if required
        if s_def != 'power':
            result.renormalize(z_new=result.z0, s_def=s_def)

        return result

    def impedance_mismatch(self, z1: NumberLike, z2: NumberLike, **kwargs) -> Network:
        r"""
        Two-port network for an impedance mismatch.

        Parameters
        ----------
        z1 : number, or array-like
            complex impedance of port 1
        z2 : number, or array-like
            complex impedance of port 2
        \*\*kwargs : key word arguments
            passed to :func:`match`, which is called initially to create a
            'blank' network.

        Returns
        -------
        mismatch : :class:`~skrf.network.Network` object
            a 2-port network representing the impedance mismatch

        Notes
        -----
        If z1 and z2 are arrays, they must be of same length
        as the :attr:`Media.frequency.npoints`

        See Also
        --------
        match
        short
        open
        load
        capacitor
        inductor
        resistor
        """
        result = self.match(nports=2, **kwargs)
        s_def = kwargs.get('s_def', S_DEF_DEFAULT)
        z1 = np.array(z1)
        z2 = np.array(z2)
        mismatch = np.broadcast_to(impedance_mismatch(z1, z2, s_def), result.s.shape)
        result.s = mismatch
        return result


    # splitter/couplers
    def tee(self, **kwargs) -> Network:
        r"""
        Ideal, lossless tee. (3-port splitter).

        Parameters
        ----------
        \*\*kwargs : key word arguments
            passed to :func:`match`, which is called initially to create a
            'blank' network.

        Returns
        -------
        tee : :class:`~skrf.network.Network` object
            a 3-port splitter

        See Also
        ----------
        splitter : this just calls splitter(3)
        match : called to create a 'blank' network
        """
        return self.splitter(3, **kwargs)

    def splitter(self, nports: int, **kwargs) -> Network:
        r"""
        Ideal, lossless n-way splitter.

        The port impedances can be mismatched and the power is split
        accordingly.

        For n > 2, the splitter is not matched because the power wave entering
        one port meet the equivalent impedance of the other ports in parallel.

        .. math::
            s_{ii} = \frac{\frac{1}{\sum_{j\neq i}\frac{1}{Z_j}}-Z_i^{*}}
            {\frac{1}{\sum_{j\neq i}\frac{1}{Z_j}}+Z_i}

        The remaining power is split between the other ports depending their
        impedances.

        .. math::
            S_{ij} = \frac{ 2 \sqrt{ {\Re}\left (Z_i \right) \cdot {\Re}\left (Z_j \right) } }
            {Z_i \cdot Z_j \cdot  \sum_{k=1 \dots  n}\frac{1}{Z_j}}

        Parameters
        ----------
        nports : int
                number of ports
        \*\*kwargs : key word arguments
                passed to :func:`match`, which is called initially to create a
                'blank' network.

        Returns
        -------
        tee : :class:`~skrf.network.Network` object
                a n-port splitter

        See Also
        --------
        match : called to create a 'blank' network
        splitter_s : actual S-parameter matrix generator algorithm.
        """
        result = self.match(nports, **kwargs)

        result.s = splitter_s(result.z0)
        return result

    # transmission line

    def to_meters(self, d: NumberLike, unit: str = 'deg') -> NumberLike:
        """
        Translate various units of distance into meters.

        This is a method of media to allow for electrical lengths as
        inputs. For dispersive media, mean group velocity is used to
        translate time-based units to distance.

        Parameters
        ----------
        d : number or array-like
            the value
        unit : str
            the unit to that x is in:
            ['deg','rad','m','cm','um','in','mil','s','us','ns','ps']

        Returns
        -------
        d_m : number, array-like
            d in meters

        See Also
        --------
        skrf.constants.to_meters
        """
        unit = unit.lower()
        #import pdb;pdb.set_trace()

        d_dict ={'deg':self.theta_2_d(d,deg=True),
                 'rad':self.theta_2_d(d,deg=False),
                 }

        if unit in d_dict:
            return d_dict[unit]
        else:
            # mean group velocity is used to translate time-based
            # units to distance
            if 's' in unit:
                # they are specifying  a time unit so calculate
                # the group velocity. (note this fails for media of
                # too little points, as it uses gradient)
                v_g = -self.v_g.imag.mean()
            else:
                v_g = c
            return to_meters(d=d,unit=unit, v_g=v_g)

    def thru(self,
             z0: NumberLike | None = None,
             **kwargs) -> Network:
        r"""
        Matched transmission line of length 0.

        Parameters
        ----------
        z0 : number, or array-like or None
            port impedance. This sets the resultant
            :class:`~skrf.network.Network`’s
            :attr:`~skrf.network.Network.z0`.
            Default is None, the Media’s :attr:`~skrf.media.Media.z0_port`
            value is used. If :attr:`~skrf.media.Media.z0_port` is
            also None (by default), in which case the Media's native
            :attr:`~skrf.media.Media.z0` is used.
            A Media's :attr:`~skrf.media.Media.z0_port` is an optional
            attribute for renormalizing resultant
            :class:`~skrf.network.Network` to a new
            reference impedance that differs from a Media's native
            :attr:`~skrf.media.Media.z0`.
        \*\*kwargs : key word arguments
            passed to :func:`match`, which is called initially to create a
            'blank' network.

        Returns
        -------
        thru : :class:`~skrf.network.Network` object
            matched transmission line of 0 length

        See Also
        --------
        line : this just calls line(0)
        open, short, match
        """
        if z0 is None:
            if self.z0_port is None:
                z0 = self.z0
            else:
                z0 = self.z0_port
        return self.line(0, z0 = z0, **kwargs)

    def line(self, d: NumberLike, unit: str = 'deg',
             z0: NumberLike | str | None = None, embed: bool = False, **kwargs) -> Network:
        r"""
        Transmission line of a given length and impedance.

        The units of `length` are interpreted according to the value
        of `unit`. If `z0` is not None, then a line specified impedance
        is produced. if `embed` is also True, then the line is embedded
        in this media's z0 environment, creating a mismatched line.

        Parameters
        ----------
        d : number
                the length of transmission line (see unit argument)
        unit : ['deg','rad','m','cm','um','in','mil','s','us','ns','ps']
                the units of d.  See :func:`to_meters`, for details
        z0 : number, string, or array-like or None
            the characteristic impedance of the line, if different
            from self.z0. To set z0 in terms of normalized impedance,
            pass a string, like `z0='1+.2j'`
        embed : bool
            deprecated parameter that only emit a warning if set to true.
        \*\*kwargs : key word arguments
            passed to :func:`match`, which is called initially to create a
            'blank' network.

        Returns
        -------
        line : :class:`~skrf.network.Network` object
            matched transmission line of given length

        Examples
        --------
        >>> my_media.line(1, 'mm', z0=100)
        >>> my_media.line(90, 'deg', z0='2') # set z0 as normalized impedance

        """
        if embed:
            # warns of deprecation
            warnings.warn(
                'Use of `embed` is deprecated and has no more effect.\n'
                'Lines are renormalized to the media port impedance `z0_port`'
                'if it is not None, otherwise they use characterisic'
                'impedance `z0`.\n'
                '`embed` will be removed in version 1.0',
              DeprecationWarning, stacklevel = 2)

        if isinstance(z0, str):
            z0 = parse_z0(z0) * self.z0

        if z0 is None:
            z0 = self.z0

        s_def = kwargs.pop('s_def', S_DEF_DEFAULT)

        # The use of either traveling or pseudo waves s-parameters definition
        # is required here.
        # The definition of the reflection coefficient for power waves has
        # conjugation.
        result = self.match(nports=2, z0 = z0, s_def='traveling', **kwargs)

        theta = self.electrical_length(self.to_meters(d=d, unit=unit))

        s11 = np.zeros(self.frequency.npoints, dtype=complex)
        s21 = np.exp(-1*theta)
        result.s = \
                np.array([[s11, s21],[s21,s11]]).transpose().reshape(-1,2,2)

        # renormalize (or embed) into z0_port if required
        if self.z0_port is not None:
            result.renormalize(self.z0_port)
        result.renormalize(result.z0, s_def=s_def)

        return result

    def line_floating(self, d: NumberLike, unit: str = 'deg',
                z0: NumberLike | str | None = None, **kwargs) -> Network:
        r"""
        Floating transmission line of a given length and impedance.

        This method returns a transmission line with floating shields.
        This is a four-port network, as opposed to the two-port _line_ method.
        Ports 1 and 3 are one side of the line, and electrically connect to
        ports 2 and 4 on the other side, respectively.

        The units of `length` are interpreted according to the value
        of `unit`. If `z0` is not None, then a line specified impedance
        is produced.

        Parameters
        ----------
        d : number
                the length of transmission line (see unit argument)
        unit : ['deg','rad','m','cm','um','in','mil','s','us','ns','ps']
                the units of d.  See :func:`to_meters`, for details
        z0 : number, string, or array-like or None
            the characteristic impedance of the line, if different
            from `media.z0`. To set z0 in terms of normalized impedance,
            pass a string, like `z0='1+.2j'`
        \*\*kwargs : key word arguments
            passed to :func:`match`, which is called initially to create a
            'blank' network.

        Returns
        -------
        line_floating : :class:`~skrf.network.Network` object
            matched, floating transmission line of given length

        Examples
        --------
        >>> my_media.line_floating(1, 'mm', z0=100)
        >>> my_media.line_floating(90, 'deg', z0='2') # set z0 as normalized impedance

        """
        if isinstance(z0, str):
            z0 = self.parse_z0(z0) * self.z0

        if z0 is None:
            z0 = self.z0

        s_def = kwargs.pop('s_def', S_DEF_DEFAULT)

        # The use of either traveling or pseudo waves s-parameters definition
        # is required here.
        # The definition of the reflection coefficient for power waves has
        # conjugation.
        result = self.match(nports=4, z0=z0, s_def='traveling', **kwargs)

        t = self.electrical_length(self.to_meters(d=d, unit=unit))

        denom = -1 + 9*np.exp(2*t)
        s11 = (1 + 3*np.exp(2*t)) / denom
        s12 = 4*np.exp(t) / denom
        s13 = (-2 + 6*np.exp(2*t)) / denom
        s14 = -s12

        result.s = np.array([[s11, s12, s13, s14],
                             [s12, s11, s14, s13],
                             [s13, s14, s11, s12],
                             [s14, s13, s12, s11]]).transpose(2, 0, 1)

        # renormalize (or embed) into z0_port if required
        if self.z0_port is not None:
            result.renormalize(self.z0_port)
        result.renormalize(result.z0, s_def=s_def)

        return result


    def delay_load(self, Gamma0: NumberLike, d: Number, unit: str = 'deg', **kwargs) -> Network:
        r"""
        Delayed load.

        A load with reflection coefficient `Gamma0` at the end of a
        matched line of length `d`.

        Parameters
        ----------
        Gamma0 : number, array-like
            reflection coefficient of load (not in dB)
        d : number
            the length of transmission line (see unit argument)
        unit : ['deg','rad','m','cm','um','in','mil','s','us','ns','ps']
            the units of d.  See :func:`to_meters`, for details
        \*\*kwargs : key word arguments
            passed to :func:`line` and :func:`load`

        Returns
        -------
        delay_load : :class:`~skrf.network.Network` object
                a delayed load

        Examples
        ----------
        >>> my_media.delay_load(-.5, 90, 'deg', Z0=50)

        Notes
        -----
        This calls ::

            line(d, unit, **kwargs) ** load(Gamma0, **kwargs)

        See Also
        --------
        line : creates the network for line
        load : creates the network for the load
        delay_short
        delay_open
        """
        return self.line(d=d, unit=unit, **kwargs) ** self.load(Gamma0=Gamma0,
                                                                **kwargs)

    def delay_short(self, d: Number, unit: str = 'deg', **kwargs) -> Network:
        r"""
        Delayed Short.

        A transmission line of given length terminated with a short.

        Parameters
        ----------
        d : number
            the length of transmission line (see unit argument)
        unit : ['deg','rad','m','cm','um','in','mil','s','us','ns','ps']
            the units of d.  See :func:`to_meters`, for details
        \*\*kwargs : key word arguments passed to :func:`delay_load`.

        Returns
        -------
        delay_short : :class:`~skrf.network.Network` object
                a delayed short

        Notes
        -----
        This calls::

                delay_load(Gamma0=-1.0, d=d, unit=unit, **kwargs)

        See Also
        --------
        delay_load
        delay_open
        """
        return self.delay_load(Gamma0=-1.0, d=d, unit=unit, **kwargs)

    def delay_open(self, d: Number, unit: str = 'deg', **kwargs) -> Network:
        r"""
        Delayed open transmission line.

        Parameters
        ----------
        d : number
            the length of transmission line (see unit argument)
        unit : ['deg','rad','m','cm','um','in','mil','s','us','ns','ps']
            the units of d.  See :func:`to_meters`, for details
        \*\*kwargs : key word arguments passed to :func:`delay_load`

        Returns
        -------
        delay_open : :class:`~skrf.network.Network` object
            a delayed open

        Notes
        -----
        This calls::

                delay_load(Gamma0=1.0, d=d, unit=unit, **kwargs)

        See Also
        --------
        delay_load
        delay_short
        """
        return self.delay_load(Gamma0=1.0, d=d, unit=unit, **kwargs)

    def shunt(self, ntwk: Network, **kwargs) -> Network:
        r"""
        Shunts a :class:`~skrf.network.Network`.

        This creates a :func:`tee` and connects
        `ntwk` to port 1, and returns the result.

        Parameters
        ----------
        ntwk : :class:`~skrf.network.Network` object
        \*\*kwargs : keyword arguments passed to :func:`tee`

        Returns
        -------
        shunted_ntwk : :class:`~skrf.network.Network` object
                a shunted a ntwk. The resultant shunted_ntwk will have
                (2 + ntwk.number_of_ports -1) ports.

        See Also
        --------
        shunt_delay_load
        shunt_delay_open
        shunt_delay_short
        shunt_resistor
        shunt_capacitor
        shunt_inductor
        """
        return connect(self.tee(**kwargs), 1, ntwk, 0)

    def shunt_delay_load(self, *args, **kwargs) -> Network:
        r"""
        Shunted delayed load.

        Parameters
        ----------
        \*args,\*\*kwargs : arguments, keyword arguments
            passed to func:`delay_load`

        Returns
        --------
        shunt_delay_load : :class:`~skrf.network.Network` object
            a shunted delayed load (2-port)

        Notes
        -----
        This calls::

                shunt(delay_load(*args, **kwargs))

        See Also
        --------
        shunt
        shunt_delay_open
        shunt_delay_short
        shunt_resistor
        shunt_capacitor
        shunt_inductor
        """
        return self.shunt(self.delay_load(*args, **kwargs), **kwargs)

    def shunt_delay_open(self,*args,**kwargs) -> Network:
        r"""
        Shunted delayed open.

        Parameters
        ----------
        \*args,\*\*kwargs : arguments, keyword arguments
            passed to func:`delay_open`

        Returns
        -------
        shunt_delay_open : :class:`~skrf.network.Network` object
            shunted delayed open (2-port)

        Notes
        -----
        This calls::

                shunt(delay_open(*args, **kwargs))

        See Also
        --------
        shunt
        shunt_delay_load
        shunt_delay_short
        shunt_resistor
        shunt_capacitor
        shunt_inductor
        """
        return self.shunt(self.delay_open(*args, **kwargs), **kwargs)

    def shunt_delay_short(self, *args, **kwargs) -> Network:
        r"""
        Shunted delayed short.

        Parameters
        ----------
        \*args,\*\*kwargs : arguments, keyword arguments
                passed to func:`delay_short`

        Returns
        -------
        shunt_delay_load : :class:`~skrf.network.Network` object
                shunted delayed open (2-port)

        Notes
        -----
        This calls::

                shunt(delay_short(*args, **kwargs))

        See Also
        --------
        shunt
        shunt_delay_load
        shunt_delay_open
        shunt_capacitor
        shunt_inductor
        """
        return self.shunt(self.delay_short(*args, **kwargs), **kwargs)

    def shunt_resistor(self, R: NumberLike, **kwargs) -> Network:
        r"""
        Shunted resistor.

        Parameters
        ----------
        R : number, array-like
            Resistor in Ohm.
        \*\*kwargs : arguments, keyword arguments
            passed to func:`resistor`

        Returns
        -------
        shunt_resistor : :class:`~skrf.network.Network` object
            shunted resistor (2-port)

        Notes
        -----
        This calls::

                shunt(resistor(R, **kwargs) ** short())

        See Also
        --------
        shunt
        shunt_delay_load
        shunt_delay_open
        shunt_delay_short
        shunt_inductor
        shunt_capacitor
        """
        return self.shunt(self.resistor(R=R, **kwargs) **
                          self.short(**kwargs), **kwargs)

    def shunt_capacitor(self, C: NumberLike, **kwargs) -> Network:
        r"""
        Shunted capacitor.

        Parameters
        ----------
        C : number, array-like
            Capacitance in Farads.
        \*\*kwargs : arguments, keyword arguments
            passed to func:`capacitor`

        Returns
        -------
        shunt_capacitor : :class:`~skrf.network.Network` object
            shunted capacitor (2-port)

        Notes
        -----
        This calls::

                shunt(capacitor(C, **kwargs) ** short())

        See Also
        --------
        shunt
        shunt_delay_load
        shunt_delay_open
        shunt_delay_short
        shunt_resistor
        shunt_inductor
        """
        return self.shunt(self.capacitor(C=C, **kwargs) **
                          self.short(**kwargs), **kwargs)

    def shunt_inductor(self, L: NumberLike, **kwargs) -> Network:
        r"""
        Shunted inductor.

        Parameters
        ----------
        L : number, array-like
            Inductance in Henries.
        \*\*kwargs : arguments, keyword arguments
            passed to func:`inductor`

        Returns
        -------
        shunt_inductor : :class:`~skrf.network.Network` object
            shunted inductor(2-port)

        Notes
        -----
        This calls::

                shunt(inductor(L, **kwargs) ** short())

        See Also
        --------
        shunt
        shunt_delay_load
        shunt_delay_open
        shunt_delay_short
        shunt_resistor
        shunt_capacitor
        """
        return self.shunt(self.inductor(L=L, **kwargs) **
                          self.short(**kwargs), **kwargs)

    def capacitor_q(self, C: NumberLike, f_0: NumberLike, q_factor: NumberLike, **kwargs) -> Network:
        r"""
        Capacitor with Q factor.

        Parameters
        ----------
        C : number, array-like
            Capacitance in Farads.
        f_0 : number
            Frequency at which Q is defined, in Hz.
        q_factor : number
            Q-factor of capacitor
        \*\*kwargs : arguments, keyword arguments
            passed to func:`capacitor`

        Returns
        -------
        capacitor_q : :class:`~skrf.network.Network` object
            capacitor_q (2-port)

        """
        idea_cap = self.shunt(self.capacitor(C=C, **kwargs), **kwargs)
        rac = q_factor / (C * 2 * np.pi * f_0)
        idea_res = self.shunt(self.resistor(R=rac), **kwargs)

        return innerconnect(connect(idea_cap, 1, idea_res, 2), 1, 3)

    def inductor_q(self,
                   L: NumberLike,
                   f_0: NumberLike,
                   q_factor: NumberLike,
                   rdc: NumberLike = 0.0,
                   **kwargs) -> Network:
        r"""
        Inductor with Q factor.

        Parameters
        ----------
        L : number, array-like
            Inductance in Henries.
        f_0 : number
            Frequency at which Q is defined, in Hz.
        q_factor : number
            Q-factor of inductor
        rdc: number, optional
            DC resistance, in Ohms. Default is 0 Ohm.
        \*\*kwargs : arguments, keyword arguments
            passed to func:`inductor`

        Returns
        -------
        inductor_q : :class:`~skrf.network.Network` object
            inductor_q (2-port)

        """
        w_q = 2 * np.pi * f_0

        if rdc == 0.0:
            rdc = 0.05 * w_q * L / q_factor

        rq1 = w_q * L / q_factor
        rq2 = np.sqrt(rq1**2 - rdc**2)
        qt = w_q * L / rq2
        rac = self.frequency.w * L / qt
        r1 = np.sqrt(rdc**2 + rac**2)

        return self.inductor(L=L, **kwargs) ** self.resistor(R=r1)

    def attenuator(self, s21: NumberLike, db: bool = True, d: Number = 0,
                   unit: str = 'deg', name: str = '', **kwargs) -> Network:
        r"""
        Ideal matched attenuator of a given length.

        Parameters
        ----------
        s21 : number, array-like
            the attenuation
        db : bool, optional
            is s21 in dB? otherwise assumes linear. Default is True (dB).
        d : number, optional
            length of attenuator. Default is 0.
        unit : ['deg','rad','m','cm','um','in','mil','s','us','ns','ps']
            the units of d.  See :func:`to_meters`, for details.
            Default is 'deg'
        name : str
            Name for the returned attenuator Network
        \*\*kwargs : arguments, keyword arguments
            passed to func:`line`

        Returns
        -------
        ntwk : :class:`~skrf.network.Network` object
            2-port attenuator

        """

        s21 = np.array(s21)
        if db:
            s21 = mf.db_2_magnitude(s21)

        result = self.match(nports=2)
        result.s[:, 0, 1] = s21
        result.s[:, 1, 0] = s21
        result = result ** self.line(d=d, unit=unit, **kwargs)
        result.name = name
        return result

    def lossless_mismatch(self, s11: NumberLike, db: bool = True, **kwargs) -> Network:
        r"""
        Lossless, symmetric mismatch defined by its return loss.

        Parameters
        ----------
        s11 : complex number, number, or array-like
            the reflection coefficient. if db==True, then phase is ignored

        db : bool, optional
            is s11 in db? otherwise assumes linear. Default is True (dB)

        \*\*kwargs : arguments, keyword arguments
            passed to func:`match`

        Returns
        -------
        ntwk : :class:`~skrf.network.Network` object
            2-port lossless mismatch

        """

        result = self.match(nports=2, **kwargs)
        s11 = np.array(s11)
        if db:
            s11 = mf.db_2_magnitude(s11)

        result.s[:, 0, 0] = s11
        result.s[:, 1, 1] = s11

        s21_mag = np.sqrt(1 - np.abs(s11) ** 2)
        s21_phase = np.angle(s11) + np.pi / 2 * (np.angle(s11) <= 0) - np.pi / 2 * (np.angle(s11) > 0)
        result.s[:, 0, 1] = s21_mag * np.exp(1j * s21_phase)
        result.s[:, 1, 0] = result.s[:, 0, 1]
        return result

    def isolator(self, source_port: int = 0, **kwargs) -> Network:
        r"""
        Two-port isolator.

        Parameters
        -------------
        source_port: int in [0,1], optional
            port at which power can flow from. Default is 0.
        \*\*kwargs : arguments, keyword arguments
            passed to func:`thru`

        Returns
        -------
        ntwk : :class:`~skrf.network.Network` object
            2-port isolator

        """
        result = self.thru(**kwargs)
        if source_port == 0:
            result.s[:, 0, 1] = 0
        elif source_port == 1:
            result.s[:, 1, 0] = 0
        return result



    ## Noisy Networks

    def white_gaussian_polar(self, phase_dev: Number, mag_dev: Number,
                             n_ports: int = 1, **kwargs) -> Network:
        r"""
        Complex zero-mean gaussian white-noise network.

        Creates a network whose s-matrix is complex zero-mean gaussian
        white-noise, of given standard deviations for phase and
        magnitude components.
        This 'noise' network can be added to networks to simulate
        additive noise.

        Parameters
        ----------
        phase_mag : number
            standard deviation of magnitude
        phase_dev : number
            standard deviation of phase
        n_ports : int
            number of ports.
        \*\*kwargs : passed to :class:`~skrf.network.Network`
            initializer

        Returns
        --------
        result : :class:`~skrf.network.Network` object
            a noise network
        """
        shape = (self.frequency.npoints, n_ports,n_ports)
        phase_rv= stats.norm(loc=0, scale=phase_dev).rvs(size = shape)
        mag_rv = stats.norm(loc=0, scale=mag_dev).rvs(size = shape)

        result = Network(**kwargs)
        result.frequency = self.frequency
        result.s = mag_rv*np.exp(1j*phase_rv)
        return result

    def random(self, n_ports: int = 1, reciprocal: bool = False, matched: bool = False,
               symmetric: bool = False, rng: None | np.random.Generator = None,
               **kwargs) -> Network:
        r"""
        Complex random network.

        Creates a n-port network whose s-matrix is filled with random
        complex numbers. Optionally, result can be matched or reciprocal.

        Parameters
        ----------
        n_ports : int
            number of ports.
        reciprocal : bool
            makes s-matrix symmetric ($S_{mn} = S_{nm}$)
        symmetric : bool
            makes s-matrix diagonal have single value ($S_{mm}=S_{nn}$)
        matched : bool
            makes diagonals of s-matrix zero
        rng : :class:`numpy.random.Generator` or None
            override the global :mod:`numpy` random number generator,
            useful for multi-threaded programs since
            :func:`skrf.mathFunctions.set_rand_rng` is not thread-safe.

        \*\*kwargs : passed to :class:`~skrf.network.Network`
                initializer

        Returns
        -------
        result : :class:`~skrf.network.Network` object
                the network
        """
        result = self.match(nports = n_ports, **kwargs)
        result.s = mf.rand_c(self.frequency.npoints, n_ports,n_ports, rng=rng)
        result.port_modes = np.array(["S"] * result.nports)
        if reciprocal and n_ports>1:
            for m in range(n_ports):
                for n in range(n_ports):
                    if m>n:
                        result.s[:,m,n] = result.s[:,n,m]
        if symmetric:
            for m in range(n_ports):
                for n in range(n_ports):
                    if m==n:
                        result.s[:,m,n] = result.s[:,0,0]
        if matched:
            for m in range(n_ports):
                for n in range(n_ports):
                    if m==n:
                        result.s[:,m,n] = 0

        return result

    ## OTHER METHODS
    def extract_distance(self, ntwk: Network) -> NumberLike:
        """
        Determines physical distance from a transmission or reflection Network.

        Given a matched transmission or reflection measurement the
        physical distance is estimated at each frequency point based on
        the scattering parameter phase of the ntwk and propagation constant.

        Notes
        -----
        If the Network is a reflect measurement, the returned distance will
        be twice the physical distance.

        Parameters
        ----------
        ntwk : `Network`
            A one-port network of either the reflection or the transmission.

        Returns
        -------
        d : number or array_like
            physical distance

        Examples
        --------
        >>> air = rf.air50
        >>> l = air.line(1, 'cm')
        >>> d_found = air.extract_distance(l.s21)
        >>> d_found
        """
        if ntwk.nports ==1:
            dphi = gradient(ntwk.s_rad_unwrap.flatten())
            dgamma = gradient(self.gamma.imag)
            return  -dphi/dgamma
        else:
            raise ValueError('ntwk must be one-port. Select s21 or s12 for a two-port.')



    def plot(self, *args, **kw):
        return self.frequency.plot(*args, **kw)



    def write_csv(self, filename: str = 'f,gamma,Z0,z0.csv'):
        """
        write this media's frequency, gamma, Z0, and z0 to a csv file.

        Parameters
        ----------
        filename : string, optional
            file name to write out data to.
            Default is 'f,gamma,Z0,z0.csv', so you probably want to specify it.

        See Also
        --------
        from_csv : class method to initialize Media object from a
            csv file written from this function
        """

        header = f'f[{self.frequency.unit}], Re(z0), Im(z0), Re(gamma), Im(gamma), Re(z0_port), Im(z0_port)\n'

        g,z,pz  = self.gamma, \
                self.z0, self.z0_port if self.z0_port is not None else self.z0

        data = np.vstack(\
                [self.frequency.f_scaled, z.real, z.imag, \
                g.real, g.imag, pz.real, pz.imag]).T

        np.savetxt(filename,data,delimiter=',',header=header)



class DefinedGammaZ0(Media):
    """
    A media directly defined by its propagation constant and characteristic impedance.

    Parameters
    ----------
    frequency : :class:`~skrf.frequency.Frequency` object or None
        frequency band of this transmission line medium.
        Default is None, which produces a 1-10ghz band with 101 points.
    z0_port : number, array-like, or None
        `z0_port` is the port impedance for networks generated by the media.
        If `z0_port` is not None, the networks generated by the media are
        renormalized (or in other words embedded) from the characteristic
        impedance Z0 of the media to `z0_port`.
        Else if `z0_port` is None, the networks port impedances will be the raw
        characteristic impedance Z0 of the media.
        (Default is None)
    gamma : number or array-like, optional
        complex propagation constant. `gamma` must adhere to
        the following convention:

            * positive real(gamma) = attenuation
            * positive imag(gamma) = forward propagation
        Default is 1j (lossless).
    z0 : number or array-like, optional.
        complex characteristic impedance of the media.
        Default is 50 ohm.
    Z0 : number, array-like, or None
        deprecated parameter, only emit a deprecation warning.
    """

    def __init__(self, frequency: Frequency | None = None,
                 z0_port: NumberLike | None = None,
                 z0: NumberLike = 50,
                 Z0: NumberLike | None = None,
                 gamma: NumberLike = 1j):
        super().__init__(frequency=frequency, z0_port=z0_port)

        self.gamma= gamma
        self.z0_characteristic = z0

        if Z0 is not None:
            # warns of deprecation
            warnings.warn(
                'Use of `Z0` in DefinedGammaZ0 initialization is deprecated.\n'
                '`Z0` has no effect. Use `z0` instead\n'
                '`Z0` will be removed in version 1.0',
              DeprecationWarning, stacklevel = 2)

    @classmethod
    def from_csv(cls, filename: str, *args, **kwargs) -> Media:
        """
        Create a Media from numerical values stored in a csv file.

        The csv file format must be written by the function :func:`write_csv`,
        or similar method which produces the following format::

            f[$unit], Re(Z0), Im(Z0), Re(gamma), Im(gamma), Re(port Z0), Im(port Z0)
            1, 1, 1, 1, 1, 1, 1
            2, 1, 1, 1, 1, 1, 1
            .....

        See Also
        --------
        write_csv
        """
        try:
            fid = open(filename)
        except(TypeError):
            # they may have passed a file
            fid = filename

        header = fid.readline()
        # this is not the correct way to do this ... but whatever
        f_unit = header.split(',')[0].split('[')[1].split(']')[0]

        f,z_re,z_im,g_re,g_im,pz_re,pz_im = \
                np.loadtxt(fid,  delimiter=',').T

        if isinstance(filename, (str, Path)):
            fid.close()

        return cls(
            frequency = Frequency.from_f(f, unit=f_unit),
            z0 = z_re+1j*z_im,
            gamma = g_re+1j*g_im,
            z0_port = pz_re+1j*pz_im,
            **kwargs
            )

    @property
    def npoints(self):
        return self.frequency.npoints

    @npoints.setter
    def npoints(self,val):
        # this is done to trigger checks on vector lengths for
        # gamma/Z0/z0
        new_freq= self.frequency.copy()
        new_freq.npoints = val
        self.frequency = new_freq


    @property
    def frequency(self):
        return self._frequency

    @frequency.setter
    def frequency(self, val):
        if hasattr(self, '_frequency') and self._frequency is not None:

            # they are updating the frequency, we may have to do something
            attrs_to_test = [self._gamma, self._z0, self._z0_port]
            if any([has_len(k) for k in attrs_to_test]):
                 raise NotImplementedError("updating a Media frequency, with non-constant "
                                           "gamma/Z0/z0 is not worked out yet")
        self._frequency = val

    @property
    def z0_characteristic(self):
        """
        Characteristic Impedance, :math:`z_0`.

        Returns
        -------
        z0_characteristic : np.ndarray
            Characteristic Impedance in units of ohms
        """
        return self._z0*ones(len(self))

    @z0_characteristic.setter
    def z0_characteristic(self, val):
        self._z0 = val

    @property
    def gamma(self):
        """
        Propagation constant.

        Returns
        ---------
        gamma : :class:`numpy.ndarray`
            complex propagation constant for this media

        Notes
        ------
        `gamma` must adhere to the following convention:

         * positive real(gamma) = attenuation
         * positive imag(gamma) = forward propagation
        """
        return self._gamma*ones(len(self))

    @gamma.setter
    def gamma(self, val):
        self._gamma = val

def has_len(x: NumberLike) -> bool:
    """
    Test of x has any length (ie is a vector).

    This is slightly non-trivial because [3] has len() but is
    doesn't really have any length.
    """
    try:
        return (len(array(x))>1)
    except TypeError:
        return False

def parse_z0(s: str) -> NumberLike:
    """
    Parse a z0 string.

    Parameters
    ----------
    s : str
        z0 string, like '50+10j'

    Returns
    -------
    z0 : np.ndarray

    Raises
    ------
    ValueError
        If could not arse the z0 string.
    """
    # they passed a string for z0, try to parse it
    re_numbers = re.compile(r'\d+')
    numbers = re.findall(re_numbers, s)
    if len(numbers)==2:
        out = float(numbers[0]) +1j*float(numbers[1])
    elif len(numbers)==1:
        out = float(numbers[0])
    else:
        raise ValueError('couldnt parse z0 string')
    return out

def get_z0_load(z0: np.ndarray,  port_idx: int) -> np.ndarray:
    r"""
    Calculate the load impedance for a given port index by parallel combining the impedances
    of all other ports.

    .. math::
        Zload_{i} = \frac{1}{\sum_{j\neq i}\frac{1}{Z_j}}

    Parameters
    ----------
    z0 : ndarray
        Port impedances, shape (n_frequencies, n_ports).
    port_idx : int
        The port index for which to calculate the load impedance.

    Returns
    -------
    z0_load : number or None
        the load impedance for the given port index, or None if the
        media does not have a load impedance for that port index.

    Raises
    ------
    IndexError
        If the port index is out of range for the given port impedances.
    """
    nports: int = z0.shape[1]

    if port_idx >= nports:
        raise IndexError(f"Port index {port_idx} is out of range for Network, which has {nports} ports.")

    if nports == 1:
        return np.ones_like(z0) * INF

    mask = np.arange(nports) != port_idx

    return 1. / np.sum(1. / z0[:, mask], axis=1)


def splitter_s(z0: np.ndarray) -> np.ndarray:
    r"""
    Generate ideal, lossless n-way splitter Network's s-parameters.

    For n > 2, the splitter is not matched because the power wave entering
    one port meet the equivalent impedance of the other ports in parallel.

    .. math::
        s_{ii} = \frac{\frac{1}{\sum_{j\neq i}\frac{1}{Z_j}}-Z_i^{*}}
        {\frac{1}{\sum_{j\neq i}\frac{1}{Z_j}}+Z_i}

    The remaining power is split between the other ports depending their
    impedances.

    .. math::
        S_{ij} = \frac{ 2 \sqrt{ {\Re}\left (Z_i \right) \cdot {\Re}\left (Z_j \right) } }
        {Z_i \cdot Z_j \cdot  \sum_{k=1 \dots  n}\frac{1}{Z_j}}

    Parameters
    ----------
    z0 : :class:`numpy.ndarray`
        Impedances of the ports

    Returns
    -------
    s : :class:`numpy.ndarray`
        S-parameters of the splitter with given impedances

    See Also
    --------
    splitter : splitter generator method.
    """
    nports = z0.shape[1]
    s = 2 * np.sqrt(np.einsum('ki,kj->kij', z0.real, z0.real)) / np.einsum('ki,kj->kij', z0, z0)
    s /= np.sum(1. / z0, axis=1)[:, None, None]

    ports_idx = np.arange(nports)
    z0_load = np.array([get_z0_load(z0=z0, port_idx=i) for i in ports_idx]).T
    s[:, ports_idx, ports_idx] = (z0_load - z0.conj()) / (z0_load + z0)

    return s
